package se.vgregion.icc;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.jdom.JDOMException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import se.vgregion.icc.handlers.ResponseHandler;
import se.vgregion.icc.riv.itintegration.monitoring.PingForConfigurationComponent;

/***
 * Helps with unmarshalling of the soap responses from PingForConfiguration responses.
 * 
 * @author ricra2
 * @see {@link se.riv.itintegration.monitoring.pingforconfigurationresponder.v1.PingForConfigurationResponseType}
 */
public final class ResponseUnmarshaller {
    private static final Logger LOG = LoggerFactory.getLogger(PingForConfigurationComponent.class);
    private static final String EXTRACT_DEPENDENCY_XML_XPATH_EXPRESSION =
        "//configuration[name='Dependency']/value/text()";
    private static final String SPRING_CONTEXT_FILE_NAME = "spring-context.xml";
    private static final String SPRING_RESPONSE_REGISTRY_BEAN_NAME = "responseRegistry";
    private static final CharSequence CONTAINS_MORE_DEPENDENCIES_IDENTIFIER = "&lt";
    private static final String EXTRACT_ROOT_NODE_XPATH_EXPRESSION = "./node()";

    private static ResponseUnmarshaller instance = null;

    private ResponseHandlerRegistry responseRegistry;

    private void setResponseRegistry(ResponseHandlerRegistry responseRegistry) {
        this.responseRegistry = responseRegistry;
    }

    private ResponseUnmarshaller() {
    }

    /***
     * Get instance of class. 
     * Spring context is loaded if needed.
     * 
     * @return ResponseUnmarshaller object.
     */
    public static ResponseUnmarshaller getInstance() {
        if (instance == null) {
            ClassPathXmlApplicationContext classPathXmlApplicationContext =
                new ClassPathXmlApplicationContext(SPRING_CONTEXT_FILE_NAME);
            ResponseHandlerRegistry bean =
                (ResponseHandlerRegistry) classPathXmlApplicationContext.getBean(SPRING_RESPONSE_REGISTRY_BEAN_NAME);
            instance = new ResponseUnmarshaller();
            instance.setResponseRegistry(bean);
        }
        return instance;
    }

    /***
     * Unmarshalls all resources in the soap response xml.
     * 
     * @param soapMessage to unmarshal from.
     * @return an List of unmarshalled Objects.
     * @throws XPathExpressionException - XPathExpressionException represents an error in an XPath expression.
     * @throws JDOMException - If errors occurs during creation of Document instance.
     * @throws IOException - If document parse of xml stream fails.
     * @throws SAXException - If document parse of xml stream fails.
     * @throws ParserConfigurationException - If document parse of xml stream fails.
     */
    public List<Object> getPingedResources(String soapMessage) throws XPathExpressionException, JDOMException,
        IOException, SAXException, ParserConfigurationException {
        List<Object> list = new ArrayList<Object>();
        List<String> resources = extractPingedResources(soapMessage);
        String classToHandle = "";

        for (String resource : resources) {
            classToHandle = extractResponseClassName(resource);
            ResponseHandler handler = responseRegistry.getResponseHandler(classToHandle);
            if (handler == null) {
                LOG.warn("Failed to load ResponseHandler from ResponseRegistry,\ncan't find implementation for \""
                    + classToHandle + "\".");
            } else {
                list.add(handler.unmarshal(resource));
            }
        }
        return list;
    }

    /***
     * Help method to extract the name of the root tag of an xml piece.
     * This is used since we need to know the tagname to get the proper ResponseHandler.
     * 
     * @param resource xml code piece.
     * @return The name of the root tag.
     * @throws XPathExpressionException - XPathExpressionException represents an error in an XPath expression.
     * @throws IOException - If document parse of xml stream fails.
     * @throws SAXException - If document parse of xml stream fails.
     * @throws ParserConfigurationException - If document parse of xml stream fails.
     */
    private String extractResponseClassName(String resource) throws ParserConfigurationException, SAXException,
        IOException, XPathExpressionException {
        DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
        DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();

        XPathFactory xpathFactory = XPathFactory.newInstance();
        XPath xpath = xpathFactory.newXPath();

        Document document = dBuilder.parse(new ByteArrayInputStream(resource.getBytes()));
        XPathExpression expression = xpath.compile(EXTRACT_ROOT_NODE_XPATH_EXPRESSION);
        Node node = (Node) expression.evaluate(document, XPathConstants.NODE);

        return node.getNodeName();
    }

    /***
     * Help method to extract each response from soap message and adds them to separate elements in a List.
     * 
     * @param soapMessage the whole soap message to extract xml pieces representing objects from.
     * @return an List<String> where each element is an pinged resource in the form of xml 
     * that can be unmarshalled.
     * @throws XPathExpressionException - XPathExpressionException represents an error in an XPath expression.
     * @throws JDOMException - If errors occurs during creation of Document instance.
     * @throws IOException - If document parse of xml stream fails.
     * @throws SAXException - If document parse of xml stream fails.
     * @throws ParserConfigurationException - If document parse of xml stream fails.
     */
    private List<String> extractPingedResources(String soapMessage) throws JDOMException, IOException,
        XPathExpressionException, SAXException, ParserConfigurationException {

        DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
        DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();

        XPathFactory xpathFactory = XPathFactory.newInstance();
        XPath xpath = xpathFactory.newXPath();
        List<String> extractedNodes = new ArrayList<String>();

        //Adds the actual response to list after removing soap tags.
        extractedNodes.add(removeSoapTags(soapMessage));

        addNodesToList(extractedNodes, soapMessage, dBuilder, xpath);

        return extractedNodes;
    }

    /***
     * Help methods to add marshalled objects to the list.
     * 
     * @param extractedNodes - List<String> of extracted nodes.
     * @param xml - to extract from
     * @param dBuilder - DocumentBuilder instance 
     * @param xpath - XPath instance
     * @throws XPathExpressionException - XPathExpressionException represents an error in an XPath expression.
     * @throws IOException - If document parse of xml stream fails.
     * @throws SAXException - If document parse of xml stream fails.
     */
    private void addNodesToList(List<String> extractedNodes, String xml, DocumentBuilder dBuilder, XPath xpath)
        throws XPathExpressionException, SAXException, IOException {
        Document document = dBuilder.parse(new ByteArrayInputStream(xml.getBytes()));
        XPathExpression expression = xpath.compile(EXTRACT_DEPENDENCY_XML_XPATH_EXPRESSION);
        NodeList nodes = (NodeList) expression.evaluate(document, XPathConstants.NODESET);

        for (int i = 0; i < nodes.getLength(); i++) {
            String value = nodes.item(i).getNodeValue().trim();
            extractedNodes.add(value);
            if (value.contains(CONTAINS_MORE_DEPENDENCIES_IDENTIFIER)) {
                addNodesToList(extractedNodes, value, dBuilder, xpath);
            }
        }
    }

    /***
     * Removes the SOAP tags from an xml String.
     * 
     * @param xmlString the String you want to manipulate
     * @return A String without the SOAP tags
     */
    private String removeSoapTags(String xmlString) {
        if (xmlString.contains("soap:Body") && xmlString.contains("soap:Envelope")) {
            xmlString = xmlString.substring(xmlString.indexOf(">") + 1);
            xmlString = xmlString.substring(xmlString.indexOf(">") + 1);
            xmlString = xmlString.substring(0, xmlString.lastIndexOf("<"));
            xmlString = xmlString.substring(0, xmlString.lastIndexOf("<"));
        }
        return xmlString;
    }
}
